#!/usr/bin/env python
# coding=utf-8
import argparse
import signal
import sys
import time
import json

from common.log import init, info, warn, debug, error
from common.rabbitmq_interface import RabbitmqService, pika
from common.utils import byteify
from common.aliyun_product_operation_interface import ALiYunImageOperation
import aliyun_image_db_model
from aliyun_image_db_model import AliyunImageDBExecutor, AliyunImageDevMapDBExecutor, \
    AliyunImageInstanceDBExecutor, AliyunImageSnapshotDBExecutor, AliyunImageSharePermissionDBExecutor
from common.setting import Cmop2ParametersAdapter, ConfigureDiscovery

ALIYUN_SERVER_LOG = "aliyun_image.log"
ALIYUN_SERVER_CONF = "service.conf"
COMPUTE_EXCHANGE = "vulcanus-iaas-computer"
LOG_EXCHANGE = "vulcanus-iaas-log"
TASK_ROUTE_KEY = "vulcanus.task.create"

def _signal_handler(signum, frame):

    info("System exit.")
    sys.exit(0)

def task_report(req, repo, route_key):

    task_rep = {}
    rs = RabbitmqService()
    debug(repo)
    task_rep["taskCode"] = repo["taskCode"]
    task_rep["action"] = route_key
    task_rep["username"] = repo["username"]
    task_rep["status"] = repo["statusCode"]
    task_rep["requestData"] = req
    task_rep["responseData"] = json.dumps(repo)
    if isinstance(repo["infos"]["requestId"], list):
        for request_id in repo["infos"]["requestId"]:
            task_rep["requestId"] = request_id
            tr_body = json.dumps(task_rep)
            rs.publish(exchange=LOG_EXCHANGE, routing_key=TASK_ROUTE_KEY, message=tr_body)
    else:
        task_rep["requestId"] = repo["infos"]["requestId"]
        tr_body = json.dumps(task_rep)
        rs.publish(exchange=LOG_EXCHANGE, routing_key=TASK_ROUTE_KEY, message=tr_body)

def rpc_query_common(func):

    def wrapper(method, properties, body):
        info("[x] Received %r" % (body))
        info("[x] Begin to do %s ..." % (method.routing_key))
        # publish message to vulcanus-iaas-log access successful
        rs = RabbitmqService()
        req = json.loads(body, object_hook=byteify)

        # convert the data to current format.
        cmop2_adapter = Cmop2ParametersAdapter()
        request_info = cmop2_adapter.input_adapt(req)

        repo = func(**request_info)
        response_info = cmop2_adapter.output_adapt(repo)

        rs.publish(exchange='', routing_key=properties.reply_to,
                         properties=pika.BasicProperties(correlation_id=properties.correlation_id),
                         message=json.dumps(response_info))

        # publish message to vulcanus-iaas-log with task report
        task_report(body, response_info, "%s.result" % (method.routing_key))

    return wrapper

@rpc_query_common
def rpc_query_images(ak, sk, region_id, **kwargs):
    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    repo = {}
    repo["Infos"] = aliy_op.DescribeImages(**kwargs)
    return repo

@rpc_query_common
def rpc_query_sharepermission(ak, sk, region_id, **kwargs):
    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    repo = {}
    repo["Infos"] = aliy_op.DescribeImageSharePermission(**kwargs)
    return repo

def rpc_process(ch, method, properties, body):

    # debug("Get rpc call body : %r" % (body))
    ALIYUN_RPC_CALLBACK_DICT = {
        "vulcanus.image.query.list": rpc_query_images,
        "vulcanus.image.query.sharepermission" : rpc_query_sharepermission
    }
    # report to log the rpc is began
    ch.basic_publish(exchange=LOG_EXCHANGE, routing_key="result.access.successful",
                     body="Accept rpc request successful!")

    if method.routing_key in ALIYUN_RPC_CALLBACK_DICT:
        evfunc = ALIYUN_RPC_CALLBACK_DICT[method.routing_key]
        evfunc(method, properties, body)
        # publish message to vulcanus-iaas-log execute complete
        ch.basic_publish(exchange=LOG_EXCHANGE, routing_key="result.access.successful",
                         body="Process %s request complete!" % (method.routing_key))
    else:
        msg = "There is no callback to process request %s." % (method.routing_key)
        ch.basic_publish(exchange=LOG_EXCHANGE, routing_key="result.access.failure",
                         body=msg)

    ch.basic_ack(delivery_tag=method.delivery_tag)

def event_process_common(func):
    def wrapper(method, properties, body):
        info("[x] Received %r" % (body))
        info("[x] Begin to do %s ..." % (method.routing_key))
        # publish message to vulcanus-iaas-log access successful
        rs = RabbitmqService()

        # get basic info
        req = json.loads(body, object_hook=byteify)

        # convert the data to current format.
        cmop2_adapter = Cmop2ParametersAdapter()
        request_info = cmop2_adapter.input_adapt(req)
        repo = func(**request_info)
        response_info = cmop2_adapter.output_adapt(repo)

        msg = json.dumps(response_info)

        # publish message to vulcanus-iaas-computer with execute result
        result_key = "%s.result" % (method.routing_key)
        rs.publish(exchange=COMPUTE_EXCHANGE, routing_key=result_key,
                   message=msg)

        # publish message to vulcanus-iaas-log with task report
        task_report(body, response_info, result_key)

    return wrapper

@event_process_common
def event_test(ak, sk, region_id, **kwargs):
    ret = {"StatusCode":"200", "RequestId":"F3E295B0-EAB1-40F2-9FC2-9784B92F5E34",
           "Message":"", "Infos":{"aaa":111}}
    repo = dict(kwargs, **ret)
    return repo

@event_process_common
def event_create_image(ak, sk, region_id, **kwargs):

    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    create_image_info = {"TaskCode" :"", "ImageId" :"", "RegionId" :"", "ImageCreateType" :"",
                         "ImageName" :"", "ImageVersion" :"", "Description" :""}
    create_image_ins_info = {"TaskCode" :"", "InstanceId" :""}
    create_image_snap_info = {"TaskCode" :"", "SysSnapshotId" :""}
    create_image_dev_map_info = {"TaskCode" :"", "SnapshotId" :"", "SnapshotSize":0}
    rs = RabbitmqService()
    debug("======== Before create %s" % (kwargs))
    ret = aliy_op.CreateImage(**kwargs)
    # check the status of the image
    for i in range(0, 60):
        s_ret = aliy_op.DescribeImages(ImageId=ret["ImageId"])
        if s_ret["Images"]["Image"]:
            image_info = s_ret["Images"]["Image"][0]
            if image_info["Status"] == "Available":
                break
            else:
                debug("====== Check image create status.")
                debug("image %s progress %s status %s." %
                      (image_info["ImageId"], image_info["Progress"],
                       image_info["Status"]))
        else:
            debug("===== Waiting for image creating...")
        rs.sleep(60)
    else:
        warn_msg = "Failed to check image creation status due to timeout."
        warn(warn_msg)
        ret["Message"] = warn_msg

    repo = dict(kwargs, **ret)
    if repo["StatusCode"] == "200":
        aliy_db_image = AliyunImageDBExecutor()
        for k, v in repo.items():
            if k in create_image_info:
                create_image_info[k] = v
            if k in create_image_ins_info:
                create_image_ins_info[k] = v
            if k in create_image_snap_info:
                create_image_snap_info[k] = v
            if k in create_image_dev_map_info:
                create_image_dev_map_info[k] = v
        if "InstanceId" in repo:
            create_image_info["ImageCreateType"] = "instance_id"
        if "SnapshotId" in repo:
            create_image_info["ImageCreateType"] = "sys_snapshot_id"
            create_image_snap_info["SysSnapshotId"] = kwargs["SnapshotId"]
        # device mapping will be implement after.

        aliy_db_image.insert(**create_image_info)

        if create_image_info["ImageCreateType"] == "instance_id":
            aliy_db_image_ins = AliyunImageInstanceDBExecutor()
            aliy_db_image_ins.insert(**create_image_ins_info)

        if create_image_info["ImageCreateType"] == "sys_snapshot_id":
            aliy_db_image_snap = AliyunImageSnapshotDBExecutor()
            aliy_db_image_snap.insert(**create_image_snap_info)
    else:
        error("Failed to create image due to %s %s." % (repo["StatusCode"], repo["Message"]))

    return repo

@event_process_common
def event_import_image(ak, sk, region_id, **kwargs):

    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    ret = aliy_op.ImportImage(**kwargs)
    repo = dict(kwargs, **ret)

    return repo

@event_process_common
def event_delete_image(ak, sk, region_id, **kwargs):

    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    ret = aliy_op.DeleteImage(**kwargs)
    repo = dict(kwargs, **ret)

    if repo["StatusCode"] == "200":
        aliy_db_image = AliyunImageDBExecutor()
        # device mapping will be implement after.
        rec = aliy_db_image.query(ImageId=kwargs["ImageId"])
        if rec:
            image_base_info = rec[0]
            aliy_db_image.delete(ImageId=kwargs["ImageId"])

            if image_base_info["ImageCreateType"] == "instance_id":
                aliy_db_image_ins = AliyunImageInstanceDBExecutor()
                aliy_db_image_ins.delete(ImageId=kwargs["ImageId"])

            if image_base_info["ImageCreateType"] == "sys_snapshot_id":
                aliy_db_image_snap = AliyunImageSnapshotDBExecutor()
                aliy_db_image_snap.delete(ImageId=kwargs["ImageId"])
        else:
            warn("Image %s has been deleted already, ignore it." % (kwargs["ImageId"]))
    else:
        error("Failed to create image due to %s %s." % (repo["StatusCode"], repo["Message"]))

    return repo

def _event_modify_common(op_func, update_keys, db_func=None, **kwargs):
    repo = {}
    kwargs_keys = set(kwargs.keys())
    k_update_keys = update_keys & kwargs_keys
    if k_update_keys:
        update_dict = {}
        for update_key in update_keys:
            update_dict[update_key] = kwargs[update_key]
        ret = op_func(**kwargs)
        if "Filters" not in kwargs:
            kwargs["Filters"] = {"ImageId": kwargs["ImageId"]}

        if db_func:
            db_func(update_dict=update_dict, **kwargs["Filters"])

        repo = dict(kwargs, **ret)
        if "Infos" not in repo:
            repo["Infos"] = {}

        for k, v in update_dict.items():
            nk = "%s%s" % (k[0].lower(), k[1:])
            repo["Infos"][nk] = v
    else:
        repo["StatusCode"] = "Error"
        repo["Message"] = "Failed to get any parameters to update (%s)." % (op_func.__name__)
        repo["Infos"] = {}
        repo["RequestId"] = ""
        repo = dict(repo, **kwargs)

    return repo

def _update_imageattribute_result(update_dict, **kwargs):
    aliy_db = AliyunImageDBExecutor()
    aliy_db.update(update_dict=update_dict, **kwargs)

def _update_sharepermission_result(update_dict, **kwargs):
    aliy_db = AliyunImageSharePermissionDBExecutor()
    aliy_db.update(update_dict=update_dict, **kwargs)

@event_process_common
def event_modify_imageattribute(ak, sk, region_id, **kwargs):
    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    update_keys = {"TaskCode", "ImageName", "Description"}
    repo = _event_modify_common(aliy_op.ModifyImageAttribute, update_keys,
                                _update_imageattribute_result, **kwargs)

    return repo

@event_process_common
def event_modify_sharepermission(ak, sk, region_id, **kwargs):
    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    update_keys = {""}
    repo = _event_modify_common(aliy_op.ModifyImageSharePermission, update_keys,
                                _update_sharepermission_result, **kwargs)

    return repo

def _event_operation_common(op_func, **kwargs):
    repo = {}
    ret = op_func(**kwargs)
    repo = dict(kwargs, **ret)

    return repo

@event_process_common
def event_copy_image(ak, sk, region_id, **kwargs):
    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    debug("Get kwargs %r." % (kwargs))
    repo = _event_operation_common(aliy_op.CopyImage, **kwargs)
    if repo["StatusCode"] == "200":
        create_image_info = {"TaskCode": "", "ImageId": "", "RegionId": "", "ImageCreateType": "copy",
                             "ImageName": "", "ImageVersion": "", "Description": ""}
        for k, v in repo.items():
            if k in create_image_info:
                create_image_info[k] = v

        aliy_db = AliyunImageDBExecutor()
        aliy_db.insert(**create_image_info)

    return repo

@event_process_common
def event_cancle_copy_image(ak, sk, region_id, **kwargs):
    aliy_op = ALiYunImageOperation(ak, sk, region_id)
    repo = _event_operation_common(aliy_op.CancelCopyImage, **kwargs)
    if repo["StatusCode"] == "200":
        aliy_db = AliyunImageDBExecutor()
        aliy_db.delete(ImageId=kwargs["ImageId"])
    return repo

def instance_event_process(ch, method, properties, body):

    ALIYUN_EVENT_CALLBACK_DICT = {
        "vulcanus.image.test": event_test,
        "vulcanus.image.create": event_create_image,
        "vulcanus.image.delete": event_delete_image,
        "vulcanus.image.modify.attribute": event_modify_imageattribute,
        "vulcanus.image.modify.sharepermission": event_modify_sharepermission,
        "vulcanus.image.operation.copy": event_copy_image,
        "vulcanus.image.operation.cancelcopy": event_cancle_copy_image,
        "vulcanus.image.import": event_import_image,
    }

    ch.basic_publish(exchange=LOG_EXCHANGE, routing_key="result.access.successful",
                     body="Accept %s request successful!" % (method.routing_key))

    if method.routing_key in ALIYUN_EVENT_CALLBACK_DICT:
        evfunc = ALIYUN_EVENT_CALLBACK_DICT[method.routing_key]
        evfunc(method, properties, body)
        # publish message to vulcanus-iaas-log execute complete
        ch.basic_publish(exchange=LOG_EXCHANGE, routing_key="result.access.successful",
                         body="Process %s request complete!" % (method.routing_key))
    else:
        msg = "There is no callback to process request %s." % (method.routing_key)
        ch.basic_publish(exchange=LOG_EXCHANGE, routing_key="result.access.failure",
                         body=msg)

    ch.basic_ack(delivery_tag=method.delivery_tag)

def start_rabbitmq_mode(args):

    host = args.rabbitmq_host
    port = args.rabbitmq_port
    user = args.rabbitmq_user
    password = args.rabbitmq_password
    rs = RabbitmqService(host, port, user, password)

    # create a channel, if one message can't be ack
    # the queue will be ignore
    rs.channel_create(prefetch_count=1)

    # create two exchanges
    # declare exchange
    rs.exchange_declare(exchange=COMPUTE_EXCHANGE, type="topic", durable=True)
    rs.exchange_declare(exchange=LOG_EXCHANGE, type="fanout", durable=True)

    # bind queue
    # args = {"x-message-ttl": 60000}
    # related route keys
    route_keys = ["vulcanus.image.create", "vulcanus.image.delete",
                  "vulcanus.image.modify.attribute", "vulcanus.image.modify.sharepermission",
                  "vulcanus.image.operation.copy", "vulcanus.image.operation.cancelcopy",
                  "vulcanus.image.import", "vulcanus.image.test"]

    q_name = "aliyun_image_service"
    rs.queue_bind(COMPUTE_EXCHANGE, route_keys=route_keys,
                  queue_name=q_name, durable=True)
    rs.regist_consume(q_name, instance_event_process)

    # bind queue for rpc
    route_keys = ["vulcanus.image.query.list", "vulcanus.image.query.sharepermission"]
    q_name = "aliyun_image_rpc"
    rs.queue_bind(COMPUTE_EXCHANGE, route_keys=route_keys,
                  queue_name=q_name, durable=True)
    rs.regist_consume(q_name, rpc_process)

    # start consuming
    rs.start_consuming()
    signal.signal(signal.SIGINT, _signal_handler)
    rs.consuming_loop()

def argument_parser():
    parser = argparse.ArgumentParser(description="Aliyun ECS Operation CLI.")

    # create actions subcommand
    sparsers = parser.add_subparsers(title="Actions", description="Actions of the service.",
                                     help="Action name, use -h after it for more details.")
    # start rabbitmq server
    sparser_rabbitmq_conn = sparsers.add_parser("start-rabbitmq-to-connect",
                                                help="Start as a worker service for API Server events exchange. Note: if you have multiple events to confim and public plse use config file.")
    sparser_rabbitmq_conn.set_defaults(func=start_rabbitmq_mode)

    sparser_rabbitmq_conn.add_argument("-H", "--host", action="store", type=str, dest="rabbitmq_host",
                                       help="Rabbitmq server host to connect.", required=False, default="localhost")
    sparser_rabbitmq_conn.add_argument("-p", "--port", action="store", type=int, dest="rabbitmq_port",
                                       help="Rabbitmq server port to connect.", required=False, default=5672)
    sparser_rabbitmq_conn.add_argument("-u", "--user", action="store", type=str, dest="rabbitmq_user",
                                       help="Rabbitmq server user to connect.", required=False, default="guest")
    sparser_rabbitmq_conn.add_argument("-P", "--password", action="store", type=str, dest="rabbitmq_password",
                                       help="Rabbitmq server password to connect.", required=False, default="guest")
    sparser_rabbitmq_conn.add_argument("-c", "--config", action="store", type=str, dest="conf_path",
                                       help="Server config file.", required=False, default=ALIYUN_SERVER_CONF)
    sparser_rabbitmq_conn.add_argument("-d", "--daemon", action="store", type=bool, dest="daemon",
                                       help="Server daemonized flag.", required=False, default=False)
    sparser_rabbitmq_conn.add_argument("-l", "--log-path", action="store", type=str, dest="log_path",
                                       help="Server log path.", required=False, default=ALIYUN_SERVER_LOG)

    return parser.parse_args()

if __name__ == "__main__":
    args = argument_parser()
    init(logpath=args.log_path)
    cd = ConfigureDiscovery()
    cmop2_adapter = Cmop2ParametersAdapter()
    cd.regist_observer("input_mapping", cmop2_adapter.input_notify)
    cd.regist_observer("output_mapping", cmop2_adapter.output_notify)
    cd.regist_observer("db", aliyun_image_db_model.notify)
    cd.start_observe_conf(conf_file=args.conf_path)
    cd.notify()
    args.func(args)
